package com.cloudfun.campusshare.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.cloudfun.campusshare.common.constant.Code;
import com.cloudfun.campusshare.common.constant.CouponType;
import com.cloudfun.campusshare.common.constant.RoleType;
import com.cloudfun.campusshare.common.constant.UserCouponStatus;
import com.cloudfun.campusshare.common.exception.BusinessException;
import com.cloudfun.campusshare.common.model.dto.UserCouponCreateDTO;
import com.cloudfun.campusshare.common.model.dto.UserCouponSearchDTO;
import com.cloudfun.campusshare.common.model.vo.UserCouponListVO;
import com.cloudfun.campusshare.common.model.vo.UserCouponStatusStatListVO;
import com.cloudfun.campusshare.common.model.vo.UserCouponVO;
import com.cloudfun.campusshare.common.model.vo.WashPointForQueryListVO;
import com.cloudfun.campusshare.common.response.PageResult;
import com.cloudfun.campusshare.common.security.AuthenticationHolder;
import com.cloudfun.campusshare.entity.UserCouponEntity;
import com.cloudfun.campusshare.entity.UserEntity;
import com.cloudfun.campusshare.entity.WashPointEntity;
import com.cloudfun.campusshare.repo.UserCouponRepo;
import com.cloudfun.campusshare.repo.UserEntityRepo;
import com.cloudfun.campusshare.repo.WashPointEntityRepo;
import com.cloudfun.campusshare.service.CreatorPersistenceService;
import com.cloudfun.campusshare.service.UserCouponService;
import com.cloudfun.campusshare.service.UserRelService;
import com.cloudfun.campusshare.service.WashPointService;
import com.cloudfun.campusshare.util.AmountUtil;
import com.cloudfun.campusshare.util.DateUtil;
import com.cloudfun.campusshare.util.QueryUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by CodeGenerator on 2020/05/29
 */
@Slf4j
@Service
public class UserCouponServiceImpl extends CreatorPersistenceService implements UserCouponService {

    @Autowired
    private UserCouponRepo userCouponRepo;

    @Autowired
    private UserEntityRepo userEntityRepo;

    @Autowired
    private WashPointEntityRepo washPointEntityRepo;

    @Autowired
    private WashPointService washPointService;

    @PersistenceContext
    private EntityManager entityManager;

    @Autowired
    private UserRelService userRelService;

    @Override
    protected Class<?> getEntityClass() {
        return UserCouponEntity.class;
    }

    @Override
    public void updateCreatorNameByCreatorId(String id, String name) {
        Query nativeQuery = entityManager.createNativeQuery("update user_coupon set issuer_name=:name where issuer_id=:id");
        nativeQuery.setParameter("name", name);
        nativeQuery.setParameter("id", id);
        nativeQuery.executeUpdate();
    }

    @Override
    public void updateCreator(String id) {
        Query nativeQuery = entityManager.createNativeQuery("update user_coupon set issuer_name=:newId where issuer_id=:oldId");
        nativeQuery.setParameter("oldId", id);
        nativeQuery.setParameter("newId", null);
        nativeQuery.executeUpdate();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // ignore
    }

    @Override
    @Transactional
    public void create(UserCouponCreateDTO createDTO) {
        String amountStr = createDTO.getAmount();
        int amount = AmountUtil.strAmount2Int(amountStr);
        // 金额小于等于10
        if (amount < 0 || amount > 10 * AmountUtil.DEFAULT_MULTIPLIER) {
            throw new BusinessException(Code.PARAMETER_ERROR, "金额输入有误");
        }
        // 时间校验
        if (createDTO.getExpirationDate().isBefore(LocalDateTime.now())) {
            throw new BusinessException(Code.PARAMETER_ERROR, "有效期输入有误");
        }
        String userPhone = createDTO.getUserPhone();
        // 只能发放给客户
        Query userIdQuery = entityManager.createNativeQuery("select u.id from user u join user_role ur on u.id=ur.user_id" +
                " join role r on ur.role_id=r.id where r.type='" + RoleType.CUSTOMER + "' and u.phone=:phone");
        userIdQuery.setParameter("phone", userPhone);
        String userId = QueryUtil.getSingleResult(userIdQuery, "");
        if (StringUtils.isBlank(userId)) {
            throw new BusinessException(Code.NODATA, "找不到用户");
        }
        List<UserCouponEntity> couponEntities = new ArrayList<>();
        for (int i = 0; i < createDTO.getCount(); i++) {
            UserCouponEntity userCouponEntity = new UserCouponEntity();
            couponEntities.add(userCouponEntity);
            userCouponEntity.setCouponId(createDTO.getCouponId());
            userCouponEntity.setPromotionId(createDTO.getPromotionId());
            userCouponEntity.setUserId(userId);
            userCouponEntity.setUserPhone(userPhone);
            userCouponEntity.setWashPointId(createDTO.getWashPointId());
            userCouponEntity.setCouponType(createDTO.getCouponType());
            userCouponEntity.setAmount(amount);
            userCouponEntity.setStartTime(LocalDateTime.now());
            userCouponEntity.setEndTime(createDTO.getExpirationDate());
            userCouponEntity.setStatus(UserCouponStatus.NOT_USED);
            userCouponEntity.setIssuerId(createDTO.getIssuerId());
            userCouponEntity.setIssuerName(createDTO.getIssuerName());
            userCouponEntity.setMerchantId(createDTO.getMerchantId());
        }
        boolean success = false;
        for (int i = 0; i < 10; i++) {
            try {
                this.fillCouponNo(couponEntities, createDTO.getCouponType());
                userCouponRepo.saveAll(couponEntities);
                success = true;
                break;
            } catch (ConstraintViolationException e) {
                log.error("创建用户代金券异常: ", e);
            }
        }
        if (!success) {
            log.error("创建用户代金券异常，尝试次数耗尽");
            throw new BusinessException(Code.ERROR, "创建异常，请稍后重试");
        }
    }

    /**
     * 填写编码
     *
     * @param userCouponEntity
     * @param couponType
     */
    private void fillCouponNo(List<UserCouponEntity> userCouponEntity, CouponType couponType) {
        LocalDateTime todayStart = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
        Long todayCount = userCouponRepo.countAllByCreateTimeGreaterThanEqual(DateUtil.toEpochMilli(todayStart) + "");
        if (todayCount == null) {
            todayCount = NumberUtils.LONG_ONE;
        }
        for (UserCouponEntity couponEntity : userCouponEntity) {
            todayCount += 1;
            // 生成编码
            String couponNo = this.genCouponNo(couponType, todayCount);
            couponEntity.setCouponNo(couponNo);
        }
    }

    /**
     * DJ+8位日期值+1位类型值（体验券为1，补偿券为2，预售券为3，活动券为4）+5位序号
     *
     * @param couponType
     * @param count
     * @return
     */
    private String genCouponNo(CouponType couponType, Long count) {
        return "DJ" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) +
                (couponType.ordinal() + 1) +
                StringUtils.leftPad(String.valueOf(count), 5, "0");
    }

    @Override
    public void delete(String string) {

    }

    @Override
    public PageResult<UserCouponVO> list(Pageable pageable, UserCouponSearchDTO searchDTO) {
        Specification<UserCouponEntity> spec = this.getSpec(searchDTO);
        Page<UserCouponEntity> page = userCouponRepo.findAll(spec, pageable);
        List<UserCouponVO> content = this.toListVO(page.getContent());
        return new PageResult<>(page.getTotalElements(), content);
    }

    @Override
    public List<UserCouponVO> listAll(UserCouponSearchDTO searchDTO) {
        Specification<UserCouponEntity> spec = this.getSpec(searchDTO);
        List<UserCouponEntity> content = userCouponRepo.findAll(spec);
        return this.toListVO(content);
    }

    private List<UserCouponVO> toListVO(List<UserCouponEntity> entities) {
        Set<String> washPointSet = new HashSet<>();
        List<UserCouponVO> content = entities.stream()
                .peek(entity -> washPointSet.add(entity.getWashPointId()))
                .map(entity -> {
                    UserCouponVO userCouponVO = new UserCouponVO();
                    BeanUtil.copyProperties(entity, userCouponVO, "amount");
                    userCouponVO.setAmount(AmountUtil.amountRoundStr(entity.getAmount(), 1));
                    return userCouponVO;
                })
                .collect(Collectors.toList());
        Map<String, String> idWpMap = washPointEntityRepo.findAllById(washPointSet).stream()
                .collect(Collectors.toMap(WashPointEntity::getId, WashPointEntity::getName));
        return content.stream()
                .peek(vo -> Optional.ofNullable(idWpMap.get(vo.getWashPointId())).ifPresent(vo::setWashPointName))
                .sorted(this.comparator())
                .collect(Collectors.toList());
    }

    private Comparator<UserCouponVO> comparator() {
        return statusComparator().thenComparing(endTimeComparator()).thenComparing(couponTypeComparator()).thenComparing(washPointComparator());
    }

    private Comparator<UserCouponVO> statusComparator() {
        return Comparator.comparingInt(vo -> vo.getStatus().ordinal());
    }

    private Comparator<UserCouponVO> endTimeComparator() {
        return Comparator.comparing(UserCouponVO::getEndTime);
    }

    private Comparator<UserCouponVO> couponTypeComparator() {
        return Comparator.comparingInt(vo -> vo.getCouponType().ordinal());
    }

    private Comparator<UserCouponVO> washPointComparator() {
        return Comparator.comparing(UserCouponVO::getWashPointId);
    }

    @Override
    public PageResult<UserCouponStatusStatListVO> statusStatisticsList(Pageable pageable, String phone) {
        List<String> washPointIdList = washPointService.getWashPointList(null, null)
                .stream().map(WashPointForQueryListVO::getId).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(washPointIdList)) {
            return new PageResult<>(NumberUtils.LONG_ZERO, new ArrayList<>());
        }
        StringBuilder pageQueryBuilder = new StringBuilder("select user_id from user_coupon where 1=1");
        if (StringUtils.isNotBlank(phone)) {
            pageQueryBuilder.append(" and user_phone=:phone");
        }
        pageQueryBuilder.append(" and wash_point_id in(:washPointIds)");
        pageQueryBuilder.append(" group by user_id limit :start,:end");
        StringBuilder countQueryBuilder = new StringBuilder("select count(*) from (select user_id from user_coupon where 1=1");
        if (StringUtils.isNotBlank(phone)) {
            countQueryBuilder.append(" and user_phone=:phone");
        }
        countQueryBuilder.append(" and wash_point_id in(:washPointIds)");
        countQueryBuilder.append(" group by user_id) as tmp");
        Query pageQuery = entityManager.createNativeQuery(pageQueryBuilder.toString());
        Query countQuery = entityManager.createNativeQuery(countQueryBuilder.toString());
        if (StringUtils.isNotBlank(phone)) {
            pageQuery.setParameter("phone", phone);
        }
        pageQuery.setParameter("washPointIds", washPointIdList);
        if (StringUtils.isNotBlank(phone)) {
            countQuery.setParameter("phone", phone);
        }
        countQuery.setParameter("washPointIds", washPointIdList);
        pageQuery.setParameter("start", pageable.getPageNumber() * pageable.getPageSize());
        pageQuery.setParameter("end", pageable.getPageSize());
        List<String> userIdList = pageQuery.getResultList();
        Map<String, UserCouponStatusStatListVO> statusStatisticsMap = this.getStatusStatisticsMap(userIdList, washPointIdList);
        List<UserCouponStatusStatListVO> dataList = userIdList.stream().map(statusStatisticsMap::get).collect(Collectors.toList());
        BigInteger count = QueryUtil.getSingleResult(countQuery, BigInteger.ZERO);

        PageResult<UserCouponStatusStatListVO> res = new PageResult<>();
        res.setElements(dataList);
        res.setTotalSize(count.longValue());
        return res;
    }

    private Map<String, UserCouponStatusStatListVO> getStatusStatisticsMap(Collection<String> userIds, List<String> washPointIdList) {
        if (CollectionUtils.isEmpty(userIds)) {
            return new HashMap<>();
        }
        Query query = entityManager.createNativeQuery("select user_id,user_phone,status from user_coupon where user_id in (:userIds) and wash_point_id in(:washPointIds)");
        query.setParameter("userIds", userIds);
        query.setParameter("washPointIds", washPointIdList);
        List<Object[]> resultList = query.getResultList();
        Map<String, UserCouponStatusStatListVO> userPhoneCouponStatusMap = new HashMap<>(resultList.size());
        resultList.forEach(tuple -> {
            String userId = (String) tuple[0];
            String userPhone = (String) tuple[1];
            String status = (String) tuple[2];
            UserCouponStatusStatListVO listVO = userPhoneCouponStatusMap.computeIfAbsent(userId, (uId) -> new UserCouponStatusStatListVO(userId, userPhone));
            UserCouponStatus userCouponStatus = UserCouponStatus.valueOf(status);
            switch (userCouponStatus) {
                case USED:
                    listVO.addUsedCount();
                    break;
                case NOT_USED:
                    listVO.addUnusedCount();
                    break;
                case EXPIRED:
                    listVO.addExpiredCount();
                    break;
                case INVALID:
                    listVO.addInvalidCount();
                    break;
            }
        });
        return userPhoneCouponStatusMap;
    }

    @Override
    public void invalid(String id) {
        UserCouponEntity userCouponEntity = userCouponRepo.findById(id).orElseThrow(() -> new BusinessException(Code.INVALID_USER));
        if (!UserCouponStatus.NOT_USED.equals(userCouponEntity.getStatus())) {
            throw new BusinessException(Code.ERROR, "代金券状态错误");
        }
        userCouponEntity.setStatus(UserCouponStatus.INVALID);
        userCouponRepo.save(userCouponEntity);
    }

    @Override
    public PageResult<UserCouponListVO> search(UserCouponSearchDTO searchDTO, Pageable pageable) {
        Specification<UserCouponEntity> specification = this.getSpec(searchDTO);
        Page<UserCouponEntity> page = userCouponRepo.findAll(specification, pageable);
        List<UserCouponListVO> content = page.getContent().stream()
                .map(entity -> {
                    UserCouponListVO listVO = new UserCouponListVO();
                    BeanUtil.copyProperties(entity, listVO);
                    if (null != entity.getWashPoint()) {
                        listVO.setWashPointName(entity.getWashPoint().getName());
                    }
                    listVO.setAmount(AmountUtil.amountRoundStr(entity.getAmount(), 2));
                    return listVO;
                }).collect(Collectors.toList());
        return new PageResult<>(page.getTotalElements(), content);
    }

    public Specification<UserCouponEntity> getSpec(UserCouponSearchDTO searchDTO) {
        Boolean viewAllData = AuthenticationHolder.getUserAuth().getViewAllData();
        return (root, query, cb) -> {
            List<Predicate> list = new ArrayList<>();
            if (AuthenticationHolder.isAdmin()) {
                if (!StringUtils.isBlank(searchDTO.getMerchantId())) {
                    list.add(cb.equal(root.get("merchantId"), searchDTO.getMerchantId()));
                }
            } else if (!AuthenticationHolder.isCustomer()) {
                if (viewAllData) {
                    String userId = AuthenticationHolder.getUserId();
                    UserEntity merchant = userRelService.getUserMerchant(userId);
                    list.add(cb.equal(root.get("merchantId"), merchant.getId()));
                } else {
                    Set<String> allSub = userRelService.getDescendantIds(AuthenticationHolder.getUserId());
                    allSub.add(AuthenticationHolder.getUserId());
                    list.add(root.get("issuerId").in(allSub));
                }
            }
            if (StringUtils.isNotBlank(searchDTO.getUserId())) {
                list.add(cb.equal(root.get("userId"), searchDTO.getUserId()));
            }
            if (CollectionUtils.isNotEmpty(searchDTO.getTypes())) {
                list.add(root.get("couponType").in(searchDTO.getTypes()));
            }
            if (CollectionUtils.isNotEmpty(searchDTO.getStatus())) {
                list.add(root.get("status").in(searchDTO.getStatus()));
            }
            if (null != searchDTO.getCreateTimeStart()) {
                list.add(cb.greaterThanOrEqualTo(root.get("startTime"), searchDTO.getCreateTimeStart()));
            }
            if (null != searchDTO.getCreateTimeEnd()) {
                list.add(cb.lessThanOrEqualTo(root.get("startTime"), searchDTO.getCreateTimeEnd()));
            }
            if (StringUtils.isNotBlank(searchDTO.getWashPointId())) {
                list.add(cb.equal(root.get("washPointId"), searchDTO.getWashPointId()));
            }
            if (StringUtils.isNotBlank(searchDTO.getKeyword())) {
                List<Predicate> like = new ArrayList<>();
                Predicate couponNoLikePredicate = cb.like(root.get("couponNo"), "%" + searchDTO.getKeyword() + "%");
                like.add(couponNoLikePredicate);
                Join<?, ?> washPoint = root.join("washPoint", JoinType.LEFT);
                Predicate washPointNameLikePredicate = cb.like(washPoint.get("name"), "%" + searchDTO.getKeyword() + "%");
                like.add(washPointNameLikePredicate);
                Predicate userPhoneLikePredicate = cb.like(root.get("userPhone"), "%" + searchDTO.getKeyword() + "%");
                like.add(userPhoneLikePredicate);
                Predicate[] predicates = new Predicate[like.size()];
                list.add(cb.or(like.toArray(predicates)));
            }

            Predicate[] predicates = new Predicate[list.size()];
            list.toArray(predicates);
            return cb.and(predicates);
        };
    }

    @Override
    @Transactional
    public Map<Code, Integer> invalid(List<String> ids) {
        List<UserCouponEntity> entities = userCouponRepo.findAllByIdIn(ids);
        return this.doInvalid(entities);
    }

    @Override
    @Transactional
    public Map<Code, Integer> invalid(UserCouponSearchDTO searchData) {
        Specification<UserCouponEntity> spec = getSpec(searchData);
        List<UserCouponEntity> entities = userCouponRepo.findAll(spec);
        return this.doInvalid(entities);
    }

    @Override
    @Transactional
    public Map<Code, Integer> invalidAll() {
        UserCouponSearchDTO searchDTO = new UserCouponSearchDTO();
        searchDTO.setMerchantId("-1");
        return this.invalid(searchDTO);
    }

    private Map<Code, Integer> doInvalid(List<UserCouponEntity> entities) {
        List<UserCouponEntity> toBeInvalid = entities.stream()
                .filter(entity -> UserCouponStatus.NOT_USED.equals(entity.getStatus()))
                .peek(entity -> entity.setStatus(UserCouponStatus.INVALID))
                .collect(Collectors.toList());
        userCouponRepo.saveAll(toBeInvalid);
        Map<Code, Integer> res = new HashMap<>();
        res.put(Code.SUCCESS, toBeInvalid.size());
        res.put(Code.ERROR, entities.size() - toBeInvalid.size());
        return res;
    }

    @Override
    public Long customerCouponCount(String userId) {
        UserCouponSearchDTO searchDTO = new UserCouponSearchDTO();
        searchDTO.setUserId(userId);
        Specification<UserCouponEntity> spec = this.getSpec(searchDTO);
        return userCouponRepo.count(spec);
    }

    @Override
    public UserCouponListVO maxAmountCoupon(UserCouponSearchDTO userCouponSearchDTO) {
        Specification<UserCouponEntity> spec = this.getSpec(userCouponSearchDTO);
        List<UserCouponEntity> entities = userCouponRepo.findAll(spec, Sort.by(Sort.Direction.DESC, "amount"));
        Optional<UserCouponEntity> first = entities.stream().findFirst();
        if (!first.isPresent()) {
            return null;
        }
        UserCouponEntity userCouponEntity = first.get();
        UserCouponListVO listVO = new UserCouponListVO();
        BeanUtil.copyProperties(userCouponEntity, listVO);
        listVO.setAmount(AmountUtil.amountRoundStr(userCouponEntity.getAmount(), 2));
        return listVO;
    }
}